// ==UserScript==
// @icon          https://www.youtube.com/favicon.ico
// @name          Youtube独轮车-Auto Youtube Chat Sender
// @author        necros & dislido
// @description   youtube 独轮车
// @match         *://www.youtube.com/*
// @version       3.2.1
// @grant         none
// @namespace https://greasyfork.org/zh-CN/users/692472-necrosn
// ==/UserScript==

{
  if (window.top !== window.self) throw new Error("非顶层框架");
  const version = "3.2.1";
  const randomInt = (min, max) => {
    if (max < min) return min;
    return Math.floor(Math.random() * (max - min + 1) + min);
  };
  const eventBus = {
    ee: new EventTarget(),
    on(type, fn, opt, fnKey = Symbol("dlc.event.type.anonymous")) {
      this.fnMap.set(fnKey, fn);
      this.ee.addEventListener(type, fn, opt);
      return fnKey;
    },
    off(type, fn, opt) {
      this.ee.removeEventListener(type, fn, opt);
    },
    emit(type, detail) {
      const event = new CustomEvent(type, { detail });
      this.ee.dispatchEvent(event);
    },
    fnMap: new Map(),
  };

  const config = {
    splitMode: 2, // 断句方式
    minCycleSec: 6, // 最小发送间隔（秒）
    maxCycleSec: 0, // 最大发送间隔（秒）
    randomDanmaku: false, // 是否随机发送
    text: "", // 发送消息列表
    maxDanmakuLength: 200, // 最大弹幕字数限制
    minDanmakuLength: 20, // 最小弹幕长度
    startTime: "2020-10-24 00:07:00",
    stopTime: "2020-10-24 00:07:00",
    splitChar: "，；：。！？…,.!?,", // 断句符
    remoteDanmakuBase: "",
    noValidate: false,
    extension: "",
    lowConsume: false,
    autoStop: true,
    cyberBattle: true,
    infoUrl: "", //api地址
    startWaitingTime: 100, //开播等待时间
    fullAutomation: false,
    lastLiveUrl: "", //上次直播地址
  };

  const refs = {
    sendBtn: null,
    chatTxtInput: null,
    autoPlayBtn: null,
    runBtn: null,
    offlineStateDiv: null,
    remoteDanmakuConfig: [],
    dlcAutomation: null,
    duluncheWnd: null,
    configValidators: [
      () =>
        Number(config.minCycleSec) < 1 &&
        eventBus.emit("setConfig.minCycleSec", 1),
      () =>
        Number(config.minDanmakuLength) < 1 &&
        eventBus.emit("setConfig.minDanmakuLength", 1),
      () =>
        Number(config.maxDanmakuLength) < config.minDanmakuLength &&
        eventBus.emit("setConfig.maxDanmakuLength", config.minDanmakuLength),
    ],
    danmakuGener: null,
    configFieldContainer: null,
    helpContent: null,
    init: async () => {
      try {
        const chatFrameCtx = document.getElementById("chatframe").contentWindow;
        refs.sendBtn = chatFrameCtx.document.querySelector(
          "#send-button button"
        ); // 发送按钮
        refs.chatTxtInput = chatFrameCtx.document.querySelector(
          "#input.yt-live-chat-text-input-field-renderer"
        ); // 输入框
        refs.offlineStateDiv = document.querySelector(
          ".ytp-offline-slate-main-text"
        ); // 直播状态
        refs.autoPlayBtn = document.querySelector("paper-toggle-button");
        if (!refs.sendBtn || !refs.chatTxtInput) return false;
        return true;
      } catch (_) {
        return false;
      }
    },
  };
  //关闭自动播放
  eventBus.on("dlc.ready", () => {
    if (refs.autoPlayBtn.active) refs.autoPlayBtn.click();
  });
  // 交换字符串中的char
  function swapStr(str, first, last){
    return str.substr(0, first)
           + str[last]
           + str.substring(first+1, last)
           + str[first]
           + str.substr(last+1);
  }
  // 预处理发送的消息
  var stopchars = ['.', '?', '!', ',', '。', '，', '、', '！', '？'];
  function text_preprocessing(text){
      const { cyberBattle } = config;
      if (!cyberBattle){
          return text;
      }
      // 移除句尾的标点符号
      while (text.length > 1 && stopchars.indexOf(text[text.length - 1]) > -1){
          text = text.substr(0, text.length - 1);
      }
      //console.log(text)
      text_split = text.split(' ');

      // 单词level的预处理
      for (let i = 0; i < text_split.length; ++i){
          // 每个单词有80%的概率被修改
          if(text_split[i].length < 3){
              continue;
          }
          //调整这一行可以随机替换单词
          //if (randomInt(1, 10) <= 8){
          if (true){
          //随机选择一个char的位置，把他与后一个字符交换，不选首字符（容易被看出来）
          index = randomInt(1, text_split[i].length - 2);
              // console.log("swap: "+i + ", index: " + index);
              text_split[i] = swapStr(text_split[i], index, index + 1);
          }
      }
      //console.log(text_split);
      text = text_split.join(' ');
      return text;
  }
  // 发送消息
  eventBus.on(
    "dlc.sendMsg",
    ({ detail: text }) => {
      refs.chatTxtInput.textContent = text_preprocessing(text);
      refs.chatTxtInput.dispatchEvent(new InputEvent("input"));
      refs.sendBtn.click();
    },
    {},
    "dlc.sendMsg.default"
  );
  eventBus.on("setRef.remoteDanmakuConfig", ({ detail }) => {
    refs.remoteDanmakuConfig = detail;
  });

  try {
    const savedCfg = JSON.parse(localStorage.getItem("duluncheCfg"));
    Object.assign(config, savedCfg);
  } catch (_) {
    // noop
  }
  let timer; // 定时器
  let apiTimer;
  //直播间加载api
  const loadLiveInfo = (url, target, ignore_err = false, started = false) => {
    fetch(url)
      .then((data) => data.json())
      .then((data) => {
        if (!data || !data.currentLives) {
          target.checked = false;
          Toast(
            h("monitor", { style: { color: "red" } }, [
              "数据格式有误， 请使用正确的api",
            ])
          );
          return;
        }
        if (started) {
          if (!data.currentLives[0])
            window.location = "https://www.youtube.com/";
          return;
        }
        if (!data.currentLives[0]) {
          Toast("监控中");
          return;
        }
        const link = data.currentLives[0].link;
        if (link === config.lastLiveUrl) {
          Toast("刚冲过了, 休息中, 等待监控API刷新");
          return;
        }
        setAndSave("lastLiveUrl", link);
        window.location = link;
      })
      .catch((err) => {
        console.error(err);
        if (!ignore_err) {
          target.checked = false;
          setAndSave("fullAutomation", false);
        }
        Toast(
          h("monitor", { style: { color: "red" } }, ["加载错误" + " " + err])
        );
        return;
      });
  };
  //保存配置
  const setAndSave = (prop, target) => {
    config[prop] = target;
    localStorage.setItem("duluncheCfg", JSON.stringify(config));
  };

  // 迷你版渲染器
  const h = (tagname, attributes = {}, children = [], option = {}) => {
    if (tagname instanceof Node) return tagname;
    if (tagname instanceof Array) {
      const frag = document.createDocumentFragment();
      tagname.forEach((it) => {
        if (it instanceof Node) {
          frag.appendChild(it);
        } else if (Array.isArray(it)) {
          frag.appendChild(h(it[0], it[1], it[2], it[3]));
        } else if (["string", "number"].includes(typeof it) || it) {
          frag.appendChild(new Text(it));
        }
      });
      return frag;
    }
    const el = document.createElement(tagname);
    Object.entries(attributes).forEach(([key, value]) => {
      if (key === "style" && typeof value === "object") {
        Object.assign(el.style, value);
      } else if (key.startsWith("$")) {
        if (typeof value === "function") {
          el.addEventListener(key.slice(1), value);
        } else {
          el.addEventListener(key.slice(1), value.handleEvent, value);
        }
      } else el.setAttribute(key, value);
    });
    if (["string", "number"].includes(typeof children)) {
      el.textContent = children;
    } else if (children) {
      el.appendChild(h(children));
    }
    if (typeof option === "function") {
      option(el);
    } else if (option.cb) {
      option.cb(el);
    }
    return el;
  };
  // 省流开关
  const savingMode = (display = true) => {
    document.querySelector(".ytp-play-button.ytp-button").click();
    if (display) {
      document.querySelector("#player.style-scope").style.display = "none";
    } else {
      document.querySelector("#player.style-scope").style.display = "block";
    }
  };
  // 格式化时间
  const formatTime = (time) => {
    const hour = Math.floor(time / 3600);
    const minute = Math.floor((time / 60) % 60);
    const second = Math.floor(time % 60);
    return `${hour}:${minute}:${second}`;
  };

  // 自动停止
  const bindStop = (eve) => {
    if (eve.target.data === "感谢收看") {
      eventBus.emit("dlc.stop");
      eventBus.emit("live.stop");
      window.location = "https://www.youtube.com";
    }
  };
  // 检测直播间状态
  const monitorLiveStatus = () => {
    setTimeout(() => {
      refs.offlineStateDiv = document.querySelector(
        ".ytp-offline-slate-main-text"
      );
      if (!refs.offlineStateDiv.innerText) {
        if (config.autoStop) {
          refs.offlineStateDiv.addEventListener("DOMNodeInserted", bindStop);
        }
        eventBus.emit("live.start");
        console.log("Live start");
      } else {
        // console.log("waiting for stream start...");
        monitorLiveStatus();
      }
    }, 10000);
  };

  eventBus.on("dlc.ready", monitorLiveStatus);
  // 各种组件
  /** toast提示组件，msg可以是渲染器children类型 */
  const Toast = (msg, alert = false, singletonId = "singletonDom") => {
    if (Toast[singletonId]) Toast[singletonId].remove();
    class_ = alert ? "dlc-alert" : "dlc-toast";
    let dom = h(
      "div",
      {
        class: class_,
        style: {
          zIndex: ++Toast.currZIndex,
        },
      },
      [
        [
          "button",
          {
            class: "dlc-btn",
            $click: () => {
              dom.remove();
              Toast[singletonId] = null;
            },
          },
          "关闭",
        ],
        ["div", {}, msg],
      ]
    );
    if (alert) setTimeout(() => dom.remove(), 10000);
    Toast[singletonId] = dom;
    document.body.appendChild(dom);
  };
  Toast.currZIndex = 10000;

  const ConfigField = (
    {
      label,
      type,
      name,
      props = {},
      children = [],
      valueProp = "value",
      helpDesc,
    },
    option
  ) => [
    "div",
    {},
    [
      helpDesc && [
        "span",
        {
          class: "help-icon",
          $click: (ev) => {
            ev.stopPropagation();
            Toast(helpDesc);
          },
        },
      ],
      [
        "label",
        {},
        [
          label,
          [
            type,
            {
              ...props,
              $change: (ev) => {
                eventBus.emit(`setConfig.${name}`, ev.target[valueProp]);
                if (props.$change) props.$change(ev);
              },
            },
            children,
            (el) => {
              el[valueProp] = config[name];
              eventBus.on(
                `setConfig.${name}`,
                ({ detail }) => {
                  config[name] = detail;
                  el[valueProp] = detail;
                },
                {},
                `dlc.setConfig.${name}.default`
              );
            },
          ],
        ],
      ],
    ],
    option,
  ];

  // 结束发射
  eventBus.on(
    "dlc.stop",
    () => {
      refs.runBtn.innerText = "出动";
      clearTimeout(timer);
      timer = null;
    },
    {},
    "dlc.stop.default"
  );

  const splitter = {
    // 单句模式
    0: (text) => [text.substr(0, config.maxDanmakuLength)],
    // 多句转轮
    2: (text) =>
      text
        .split("\n")
        .map((it) => it.trim().substr(0, config.maxDanmakuLength))
        .filter(Boolean),
    // 说书模式
    1: (text) => {
      const { maxDanmakuLength, minDanmakuLength, splitChar } = config;
      const list = [];
      text
        .trim()
        .replace(/\s+/g, " ")
        .split(new RegExp(`(?<=[${splitChar.replace(/(\\|])/g, "\\$1")}])`))
        .reduce((buf, curr, currIndex, arr) => {
          buf += curr;
          while (buf.length > maxDanmakuLength) {
            list.push(buf.substr(0, maxDanmakuLength));
            buf = buf.substr(maxDanmakuLength);
          }
          if (currIndex === arr.length - 1) {
            list.push(buf);
            return "";
          }
          if (buf.length < minDanmakuLength) return buf;
          list.push(buf);
          return "";
        }, "");
      return list;
    },
  };
  // 发射弹幕
  eventBus.on(
    "dlc.run",
    () => {
      const {
        maxCycleSec,
        minCycleSec,
        text,
        splitMode,
        randomDanmaku,
      } = config;

      // 检查设置项
      if (!config.noValidate) {
        try {
          refs.configValidators.forEach((vali) => vali());
        } catch (err) {
          Toast(err);
        }
      }

      localStorage.setItem("duluncheCfg", JSON.stringify(config));

      const localDanmakuList = splitter[splitMode](text);
      const danmakuList = refs.remoteDanmakuConfig
        .filter(Boolean)
        .reduce((list, data) => list.concat(data.list), localDanmakuList);

      if (!danmakuList.length) {
        Toast("当前弹幕列表为空！");
        return;
      }

      refs.runBtn.innerText = "中止";

      const minCycleTime = parseInt(minCycleSec * 1000, 10);
      const maxCycleTime = parseInt(maxCycleSec * 1000, 10);

      refs.danmakuGener = (function* gen() {
        if (+splitMode === 2 && randomDanmaku) {
          while (true) yield danmakuList[randomInt(0, danmakuList.length - 1)];
        } else {
          while (true) yield* danmakuList;
        }
      })();

      const nextTimer = () => {
        timer = setTimeout(async () => {
          eventBus.emit("dlc.sendMsg", (await refs.danmakuGener.next()).value);
          if (timer) nextTimer();
        }, randomInt(minCycleTime, maxCycleTime));
      };
      nextTimer();
    },
    {},
    "dlc.run.default"
  );

  // 控制台
  refs.duluncheWnd = h("div", { class: "dlc-cmd" }, [
    [
      "div",
      {
        class: "dlc-titlebar",
        $mousedown(ev) {
          if (ev.target !== this) return;
          const mask = h("div", {
            style: {
              position: "fixed",
              left: "0",
              top: "0",
              width: "100vw",
              height: "100vh",
            },
          });
          this.style.cursor = "all-scroll";
          document.body.appendChild(mask);
          const { layerX, layerY } = ev;
          const move = (e) => {
            refs.duluncheWnd.style.left = `${e.clientX - layerX}px`;
            refs.duluncheWnd.style.top = `${e.clientY - layerY}px`;
          };
          document.addEventListener("mousemove", move);
          document.addEventListener(
            "mouseup",
            () => {
              document.removeEventListener("mousemove", move);
              this.style.cursor = "";
              mask.remove();
            },
            { once: true }
          );
        },
      },
      [
        [
          "button",
          {
            class: "dlc-btn",
            $click: (ev) => {
              ev.stopPropagation();
              if (refs.runBtn.innerText === "出动") eventBus.emit("dlc.run");
              else eventBus.emit("dlc.stop");
            },
          },
          "出动",
          (el) => {
            refs.runBtn = el;
          },
        ],
        version,
        [
          "span",
          {
            class: "help-icon",
            $click: (ev) => {
              ev.stopPropagation();
              Toast(refs.helpContent);
            },
          },
        ],
        [
          "div",
          {
            class: "dlc-close-btn",
            $click: (ev) => {
              ev.stopPropagation();
              refs.duluncheWnd.style.setProperty("display", "none");
            },
          },
          "X",
        ],
      ],
    ],
    [
      "div",
      { style: { margin: "0 auto" } },
      [
        ConfigField({
          label: "",
          name: "text",
          type: "textarea",
          props: {
            placeholder: "输入需要发射的内容到这里哦",
            style: {
              width: "265px",
              height: "155px",
              overflow: "scroll",
              whiteSpace: "pre",
            },
          },
        }),
        ConfigField({
          label: "最小间隔时间(s):",
          name: "minCycleSec",
          type: "input",
          props: {
            type: "number",
            min: 3,
            placeholder: 3,
            style: { width: "48px", margin: "1px" },
          },
        }),
        ConfigField({
          label: "最大间隔时间(s):",
          name: "maxCycleSec",
          type: "input",
          props: {
            type: "number",
            min: 3,
            placeholder: 3,
            style: { width: "48px", margin: "1px" },
          },
          helpDesc:
            "如果设置小于最小间隔时间，则相当于不随机间隔，以最小间隔时间发送弹幕",
        }),
        // ConfigField({
        //   label: "弹幕长度上限:",
        //   name: "maxDanmakuLength",
        //   type: "input",
        //   props: {
        //     type: "number",
        //     min: 1,
        //     style: { width: "48px", margin: "1px" },
        //   },
        // }),

        ConfigField({
          label: "断句方式:",
          name: "splitMode",
          type: "select",
          children: [
            { value: "2", text: "多句转轮" },
            { value: "0", text: "单句模式" },
            { value: "1", text: "说书模式" },
          ].map(({ text, value }) => ["option", { value }, text]),
          helpDesc:
            "多句转轮:每条弹幕一行；单句模式:不断行；说书模式:按表单符号与说书长度下限断行",
        }),
        ConfigField(
          {
            label: "随机弹幕:",
            name: "randomDanmaku",
            type: "input",
            props: { type: "checkbox" },
            valueProp: "checked",
            helpDesc:
              "随机弹幕: 是否随机从弹幕列表选取弹幕发送，仅用于多句转轮模式；在说书模式启用会导致内容混乱",
          },
          (el) => {
            if (+config.splitMode !== 2) el.classList.add("hide");
            eventBus.on(
              "setConfig.splitMode",
              ({ detail: value }) => {
                if (value !== "2") el.classList.add("hide");
                else el.classList.remove("hide");
              },
              {},
              "dlc.setConfig.splitMode.randomDanmakuField"
            );
          }
        ),
        ConfigField({
          label: "关闭配置项校验:",
          name: "noValidate",
          type: "input",
          props: { type: "checkbox" },
          valueProp: "checked",
          helpDesc:
            "不对输入的配置项进行校验，这意味着你可以进行将间隔时间设置为0这样的危险操作",
        }),
        ConfigField(
          {
            label: "说书长度下限:",
            name: "minDanmakuLength",
            type: "input",
            props: {
              type: "number",
              min: 1,
              style: { width: "48px", margin: "1px" },
            },
            helpDesc:
              "说书长度下限: 仅说书模式生效，断句时会尽量控制弹幕长度在此之上",
          },
          (el) => {
            if (+config.splitMode !== 1) el.classList.add("hide");
            eventBus.on(
              "setConfig.splitMode",
              ({ detail: value }) => {
                if (value !== "1") el.classList.add("hide");
                else el.classList.remove("hide");
              },
              {},
              "dlc.setConfig.splitMode.minDanmakuLengthField"
            );
          }
        ),
        ConfigField(
          {
            label: "说书断句符:",
            name: "splitChar",
            type: "input",
            props: {
              type: "text",
              min: 1,
              style: { width: "48px", margin: "1px" },
            },
            helpDesc:
              "说书断句符: 说书模式下以配置的符号分割文章为多条弹幕后，再合并到说书长度下限以上的弹幕长度，为空时固定按说书长度下限断句",
          },
          (el) => {
            if (+config.splitMode !== 1) el.classList.add("hide");
            eventBus.on(
              "setConfig.splitMode",
              ({ detail: value }) => {
                if (value !== "1") el.classList.add("hide");
                else el.classList.remove("hide");
              },
              {},
              "dlc.setConfig.splitMode.splitCharField"
            );
          }
        ),
        ConfigField(
          {
            label: "（测试）赛博大战",
            name: "cyberBattle",
            type: "input",
            props: { type: "checkbox" },
            valueProp: "checked",
            helpDesc: "发射的弹药会被预处理以对抗Nightbot",
          },
          () => {
            const { cyberBattle } = config;
            eventBus.on("setConfig.cyberBattle", ({ detail: value }) => {
              console.log("cyberBattle: " + value);
            });
          }
        ),
        ConfigField(
          {
            label: "省流模式",
            name: "lowConsume",
            type: "input",
            props: { type: "checkbox" },
            valueProp: "checked",
            helpDesc: "只看聊天框，节省流量",
          },
          () => {
            const { lowConsume } = config;
            if (lowConsume) {
              eventBus.on("dlc.ready", savingMode);
            }
            eventBus.on("setConfig.lowConsume", ({ detail: value }) => {
              savingMode(value);
            });
          }
        ),
        ConfigField(
          {
            label: "自动停车",
            name: "autoStop",
            type: "input",
            props: { type: "checkbox" },
            valueProp: "checked",
            helpDesc: "自动刹车，可能有bug",
          },
          () => {
            // const { autoStop } = config;
            eventBus.on("setConfig.autoStop", ({ detail: value }) => {
              if (value) {
                refs.offlineStateDiv.addEventListener(
                  "DOMNodeInserted",
                  bindStop
                );
              } else {
                try {
                  refs.offlineStateDiv.removeEventListener(
                    "DOMNodeInserted",
                    bindStop
                  );
                } catch {
                  // noop

                }
              }
            });
          }
        ),
        ConfigField(
          {
            label: h([
              "全自动发车:",

              [
                "input",
                {
                  type: "checkbox",
                  $click: ({ target }) => {
                    const { infoUrl } = config;
                    if (!target.checked) {
                      clearInterval(apiTimer);
                      setAndSave("fullAutomation", false);
                      return;
                    }
                    setAndSave("fullAutomation", true);

                    if (window.location.href !== "https://www.youtube.com/") {
                      Toast("只能在油管首页开启监控, 回到首页刷新即可");
                      return;
                    }
                    Toast("监控中...");
                    loadLiveInfo(infoUrl, target);
                    apiTimer = setInterval(
                      () => loadLiveInfo(infoUrl, target, (ignore_err = true)),
                      60000
                    );
                  },
                },
              ],
            ]),
            name: "infoUrl",
            type: "textarea",
            props: {
              placeholder: "开播监控地址",
              style: {
                width: "265px",
                height: "40px",
                overflow: "scroll",
                whiteSpace: "pre",
              },
            },
            helpDesc: "填监控API地址，如果不知道是什么，去NGA翻独轮车发布贴",
          },
          (el) => {
            const { fullAutomation, infoUrl, lastLiveUrl } = config;
            let target = el.querySelector("input");
            if (window.location.href === lastLiveUrl) {
              setInterval(
                () =>
                  loadLiveInfo(
                    infoUrl,
                    target,
                    (ignore_err = true),
                    (started = true)
                  ),
                60000
              );
            }

            if (fullAutomation) {
              target.checked = true;
            }
            if (
              fullAutomation &&
              infoUrl !== "" &&
              window.location.href === "https://www.youtube.com/"
            ) {
              target.checked = true;
              Toast("监控中...");
              loadLiveInfo(infoUrl, target);
              apiTimer = setInterval(
                () => loadLiveInfo(infoUrl, target, (ignore_err = true)),
                60000
              );
            }
          }
        ),
        ConfigField(
          {
            label: "开播等待时间(s)",
            name: "startWaitingTime",
            type: "input",
            props: {
              type: "number",
              min: 5,
              placeholder: config.startWaitingTime,
              style: { width: "48px", margin: "1px" },
            },
          },
          () => {
            const { fullAutomation } = config;
            const waitingForRun = () => {
              const { startWaitingTime } = config;
              Toast(
                `独轮车将于${startWaitingTime}秒后启动，如果这不是你的本意，请关闭全自动发车`,
                true
              );
              setTimeout(() => {
                eventBus.emit("dlc.run");
              }, startWaitingTime * 1000);
            };

            if (fullAutomation) {
              eventBus.on("live.start", waitingForRun);
            }
          }
        ),

        ConfigField({
          label: h([
            [
              "button",
              {
                class: "dlc-btn",
                $click: ({ target }) => {
                  if (target.innerText !== "定时启动") return;
                  const { startTime } = config;
                  const timeStamp = Date.parse(new Date(startTime));
                  let timeRemain = timeStamp - new Date().getTime();
                  target.innerText = formatTime(parseInt(timeRemain / 1000));
                  const startTimer = () => {
                    setTimeout(() => {
                      if (timeRemain > 0) {
                        timeRemain = timeStamp - new Date().getTime();
                        target.innerText = formatTime(
                          parseInt(timeRemain / 1000)
                        );
                        startTimer();
                      } else {
                        eventBus.emit("dlc.run");
                        target.innerText = "定时启动";
                      }
                    }, 1000);
                  };
                  startTimer();
                },
              },
              "定时启动",
            ],
          ]),
          name: "startTime",
          type: "input",
          props: {
            type: "text",
            min: 1,
            style: { width: "150px", margin: "1px" },
          },
          // helpDesc: '输入时间定时启动，格式举例：2020-10-21 17:31:00',
        }),
        ConfigField({
          label: h([
            [
              "button",
              {
                class: "dlc-btn",
                $click: ({ target }) => {
                  if (target.innerText !== "定时结束") return;
                  const { stopTime } = config;
                  const timeStamp = Date.parse(new Date(stopTime));
                  let timeRemain = timeStamp - new Date().getTime();
                  target.innerText = formatTime(parseInt(timeRemain / 1000));
                  const stopTimer = () => {
                    setTimeout(() => {
                      if (timeRemain > 0) {
                        timeRemain = timeStamp - new Date().getTime();
                        target.innerText = formatTime(
                          parseInt(timeRemain / 1000)
                        );
                        stopTimer();
                      } else {
                        eventBus.emit("dlc.stop");
                        target.innerText = "定时结束";
                      }
                    }, 1000);
                  };
                  stopTimer();
                },
              },
              "定时结束",
            ],
          ]),
          name: "stopTime",
          type: "input",
          props: {
            type: "text",
            min: 1,
            style: { width: "150px", margin: "1px" },
          },
          // helpDesc: '输入时间定时结束，格式举例：2020-10-21 17:31:00',
        }),
        h([
          [
            "button",
            {
              class: "dlc-btn",
              $click: ({ target }) => {
                localStorage.setItem("duluncheCfg", JSON.stringify(config));
                Toast("保存成功");
              },
            },
            "保存配置",
          ],
        ]),
        ConfigField({
          label: h([
            "加载远程弹幕库:",
            [
              "button",
              {
                class: "dlc-btn",
                $click: ({ target }) => {
                  if (target.innerText !== "更新") return;
                  target.innerText = "更新中...";
                  const { remoteDanmakuBase } = config;
                  const urlList = remoteDanmakuBase
                    .split("\n")
                    .map((it) => it.trim())
                    .filter(Boolean);
                  const queued = new Set();
                  const allRemoteUrl = new Set();
                  const loaded = [];
                  const loadFinish = () => {
                    eventBus.emit("setRef.remoteDanmakuConfig", loaded);
                    target.innerText = "更新";
                    Toast(
                      h(
                        "pre",
                        { style: { color: "blue" } },
                        refs.remoteDanmakuConfig
                          .map(
                            (data) =>
                              data.error ||
                              `${data.name || "匿名弹幕库"}: ${
                                data.list.length
                              }条`
                          )
                          .join("\n")
                      )
                    );
                  };
                  const loadRemoteDanmaku = (url) => {
                    if (allRemoteUrl.has(url)) return;
                    queued.add(url);
                    allRemoteUrl.add(url);
                    fetch(url)
                      .then((data) => data.json())
                      .then((data) => {
                        if (!data) {
                          loaded.push({ error: `[获取失败]${url}` });
                          return;
                        }
                        if (Array.isArray(data.extends)) {
                          data.extends.forEach((extUrl) =>
                            loadRemoteDanmaku(extUrl)
                          );
                        }
                        if (Array.isArray(data.list)) loaded.push(data);
                      })
                      .catch((err) => {
                        console.error(err);
                        loaded.push({ error: `[获取失败]${url}` });
                      })
                      .finally(() => {
                        queued.delete(url);
                        if (queued.size === 0) loadFinish();
                      });
                  };
                  urlList.forEach((url) => loadRemoteDanmaku(url));
                },
              },
              "更新",
            ],
            [
              "span",
              {},
              "(未加载弹幕库)",
              (el) => {
                eventBus.on("setRef.remoteDanmakuConfig", ({ detail }) => {
                  const totalLength = detail.reduce(
                    (total, data) => total + ((data && data.list.length) || 0),
                    0
                  );
                  el.innerText = `(已加载${totalLength}条)`;
                });
              },
            ],
          ]),
          name: "remoteDanmakuBase",
          type: "textarea",
          props: {
            placeholder: "支持多个网址，每个一行",
            style: {
              width: "265px",
              height: "70px",
              overflow: "scroll",
              whiteSpace: "pre",
            },
          },
          helpDesc:
            "从指定地址加载弹幕列表，支持添加多个地址，每个一行；远程弹幕列表会添加到本地弹幕列表后；更新的弹幕会在下次出动时生效",
        }),
      ],
      (el) => {
        refs.configFieldContainer = el;
      },
    ],
  ]);

  // 默认悬浮窗
  let tip = false;
  const suspension = h(
    "div",
    {
      class: "dlc-suspension",
      $click: () => {
        refs.duluncheWnd.style.setProperty("display", "block");
        if (!tip) {
          tip = true;
          Toast(
            "多句转轮模式每句之间请用回车分隔，为了自己的账号，建议发言间隔调至8000以上;"
          );
        }
      },
    },
    "初始化中...",
    (el) => {
      eventBus.on(
        "dlc.ready",
        () => {
          el.textContent = "独轮车控制台";
        },
        { once: true },
        "dlc.ready.default"
      );
    }
  );

  window.dulunche = {
    version,
    config: new Proxy(config, {
      set(_, p, value) {
        eventBus.emit(`setConfig.${p}`, value);
      },
    }),
    eventBus,
    refs,
    components: {
      Toast,
      ConfigField,
      h,
    },
  };

  if (config.extension) {
    try {
      eval(config.extension);
    } catch (e) {
      console.error(e);
      Toast("扩展脚本运行失败，错误信息已输出到控制台");
    }
  }

  const init = async () => {
    let result;
    try {
      result = await refs.init();
    } catch (_) {
      // noop
    }
    if (result !== true) {
      setTimeout(() => init(), 1000);
    } else {
      eventBus.emit("dlc.ready");
    }
  };
  init();
  // 窗口宽度变化会导致聊天栏重新加载
  setInterval(() => {
    try {
      const chatFrameCtx = document.getElementById("chatframe").contentWindow;
      const sendBtn = chatFrameCtx.document.querySelector(
        "#send-button button"
      ); // 发送按钮
      const chatTxtInput = chatFrameCtx.document.querySelector(
        "#input.yt-live-chat-text-input-field-renderer"
      ); // 输入框
      if (sendBtn) refs.sendBtn = sendBtn;
      if (chatTxtInput) refs.chatTxtInput = chatTxtInput;
    } catch (_) {
      // noop
    }
  }, 60000);

  document.body.appendChild(suspension);
  document.body.appendChild(refs.duluncheWnd);

  document.body.appendChild(
    h(
      "style",
      {},
      `
    .dlc-cmd {
      background: #FFFFFF;
      overflow-y: auto;
      overflow-x: hidden;
      z-index: 998;
      position: fixed;
      padding:5px;
      width: 290px;
      height: 510px;
      box-sizing: content-box;
      border: 1px solid #ff921a;
      border-radius: 5px;
      right: 10px;
      top: 30%;
      display: none;
    }

    .dlc-titlebar {
      user-select: none;
      background-color: #fb5;
    }

    .dlc-close-btn {
      display: inline-block;
      margin-top: 3px;
      position: relative;
      text-align: center;
      width: 19px;
      height: 19px;
      color: white;
      cursor: pointer;
      float: right;
      margin-right: 5px;
      background-color: black;
      border: gray 1px solid;
      line-height: 21px;
    }
    .dlc-btn {
      display: inline-block;
      background: #f70;
      color: #FFFFFF;
      min-width: 70px;
      height: 24px;
      margin: 2px;
    }

    .dlc-suspension {
      background: #1A59B7;
      color:#ffffff;
      overflow: hidden;
      z-index: 997;
      position: fixed;
      padding:5px;
      text-align:center;
      width: 85px;
      height: 22px;
      border-radius: 5px;
      right: 10px;
      top: 30%;
    }

    .dlc-toast {
      top: 10px;
      left: 10px;
      max-height: 90vh;
      max-width: 60vw;
      overflow: auto;
      position: fixed;
      background: lightgrey;
      padding: 16px;
      border: gray 1px solid;
    }

    .dlc-alert {
      max-height: 90vh;
      max-width: 60vw;
      overflow: auto;
      background: lightgrey;
      padding: 16px;
      border: gray 1px solid;
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%,-50%);
    }
    .help-icon::after {
      content: '?';
      margin-right: 4px;
      display: inline-block;
      background-color: #ddd;
      border-radius: 50%;
      width: 16px;
      height: 16px;
      line-height: 16px;
      text-align: center;
      border: #999 1px solid;
      font-size: 12px;
    }

    .hide {
      display: none;
    }
  `
    )
  );

  refs.helpContent = [
    [
      "button",
      {
        class: "dlc-btn",
        $click: () => {
          Toast([
            [
              "button",
              {
                class: "dlc-btn",
                $click: () => {
                  localStorage.setItem("duluncheCfg", JSON.stringify(config));
                  Toast("保存成功", "saveExtensionConfig");
                },
              },
              "保存",
            ],
            "扩展脚本修改后，需要保存并刷新页面生效",
            ConfigField({
              label: "",
              name: "extension",
              type: "textarea",
              props: {
                placeholder: "扩展脚本，这里的代码会在独轮车初始化前eval执行",
                style: {
                  width: "100%",
                  height: "300px",
                  overflow: "scroll",
                  whiteSpace: "pre",
                },
              },
            }),
          ]);
        },
      },
      "编辑扩展脚本",
    ],
    [
      "pre",
      {},
      `
API开发指南，在window.dulunche获取引用，TS定义如下
/**
 * 独轮车配置，这是一个Proxy对象，你可以直接获取和设置配置，通过这个对象设置配置项时，也会向eventBus提交一个emit(\`setConfig.\${配置项名称}\`, value)事件
 * config对象在每次启动时会保存到localStorage的duluncheCfg中
 */
interface ConfigType {
  /** 断句方式0: 单句模式 1: 说书模式 2: 多句转轮 */
  splitMode: number;
  /** 最小发送间隔（秒） */
  minCycleSec: number;
  /** 最大发送间隔（秒） */
  maxCycleSec: number;
  /** 是否随机发送 */
  randomDanmaku: boolean;
  /** 发送消息列表 */
  text: string;
  /** 最大弹幕字数限制 */
  maxDanmakuLength: number;
  /** 最小弹幕长度 */
  minDanmakuLength: number;
  /** 断句符 */
  splitChar: string;
  /** 远程弹幕库 */
  remoteDanmakuBase: string;
  /** 不校验配置，即不运行configValidators, 设为true是，所有配置项都不会限制输入范围 */
  noValidate: boolean;
  /** 扩展脚本，独轮车初始化前会eval执行这个脚本 */
  extension: string;

  [key: string]: any;
}

/**
 * 独轮车事件总线，可以在这里监听和触发各种事件
 */
interface DuluncheEventBus {
  /** 用来绑定事件的EventTarget对象，通常不需要直接操作这个对象 */
  ee: EventTarget;
  /** 监听事件，addEventListener, 会返回一个key用于从fnMap获取监听器引用，这个key可以手动指定，默认是一个Symbol */
  on<T = any>(
    type: string,
    fn: (detail: CustomEvent<T>) => void,
    opt?: boolean | AddEventListenerOptions, fnKey?: string | Symbol
  ): string | Symbol;
  /** 取消监听事件，removeEventListener */
  off<T = any>(type: string, fn: (detail: CustomEvent<T>) => void, opt?: boolean | AddEventListenerOptions): any;
  /** 触发事件，dispatchEvent */
  emit<T = any>(type: string, detail: T): void;
  /** 已经注册的事件监听器map，可以通过key拿到事件监听器的引用 */
  fnMap: Map<string | Symbol, (detail: CustomEvent) => void>;
}

/** 独轮车内部一些引用，修改它们可以改变独轮车原本的一些行为 */
interface DuluncheRefs {
  /** 配置项校验器，独轮车会在dlc.run开始时依次执行数组中的每个函数 */
  configValidators: Array<() => void>
  /** 独轮车控制台窗口dom引用 */
  duluncheWnd: HTMLDivElement;
  /** 独轮车配置列表dom引用 */
  configFieldContainer: HTMLDivElement;
  /** 弹幕生成器，独轮车通过调用danmakuGener.next().value来获取下一条要发送的弹幕，弹幕生成器只能在独轮车启动状态下被替换，最早是dlc.run的下一次事件循环 */
  danmakuGener: GeneratorFunction;
  /** 独轮车帮助信息内容 */
  helpContent: any;
  /** 初始化函数，若没有返回true则会等待1秒后重新执行，初始化成功会触发dlc.ready事件，默认行为是获取油管直播页聊天输入框和发送按钮引用，可以是异步函数 */
  init: () => boolean | Promise<boolean>;

  [key: string]: any;
}

interface Window {
  dulunche: {
    config: ConfigType;
    eventBus: DuluncheEventBus,
    refs: DuluncheRefs,

    /** 组件和渲染器，建议看过源码后使用 */
    components: {
      Toast: (children: string | Node, singletonId?: string) => void;
      ConfigField: (...args: any) => HTMLDivElement;
      h: (...args: any) => Node;
    }
  };
}

/** 
eventBus 默认的事件列表, 你可以通过emit主动触发事件，或通过on监听事件
事件名 | 事件 | detail参数类型 | detail参数说明
dlc.run | 启动独轮车 | void | -
dlc.stop | 停止独轮车 | void | -
dlc.sendMsg | 发送消息 | string | 发送的内容
dlc.ready | 初始化成功 | void | -
setConfig.\${configKey} | 修改配置 | any | 修改的配置值
live.start| 监控到直播已经开始 | void
live.stop | 监控到直播已经结束 | void
 */
 `,
    ],
  ];
}
